export const codeBackslash = 0x5c // "\"
export const codeSlash = 0x2f // "/"
export const codeAsterisk = 0x2a // "*"
export const codeOpeningBrace = 0x7b // "{"
export const codeClosingBrace = 0x7d // "}"
export const codeOpeningBracket = 0x5b  // "["
export const codeClosingBracket = 0x5d // "]"
export const codeOpenParenthesis = 0x28 // "("
export const codeCloseParenthesis = 0x29 // ")"
export const codeSpace = 0x20 // " "
export const codeNewline = 0xa // "\n"
export const codeTab = 0x9 // "\t"
export const codeReturn = 0xd // "\r"
export const codeBackspace = 0x08 // "\b"
export const codeFormFeed = 0x0c // "\f"
export const codeDoubleQuote = 0x0022 // "
export const codePlus = 0x2b // "+"
export const codeMinus = 0x2d // "-"
export const codeQuote = 0x27 // "'"
export const codeZero = 0x30 // "0"
export const codeNine = 0x39 // "9"
export const codeComma = 0x2c // ","
export const codeDot = 0x2e // "." (dot, period)
export const codeColon = 0x3a // ":"
export const codeSemicolon = 0x3b // ";"
export const codeUppercaseA = 0x41 // "A"
export const codeLowercaseA = 0x61 // "a"
export const codeUppercaseE = 0x45 // "E"
export const codeLowercaseE = 0x65 // "e"
export const codeUppercaseF = 0x46 // "F"
export const codeLowercaseF = 0x66 // "f"
const codeNonBreakingSpace = 0xa0
const codeEnQuad = 0x2000

const codeHairSpace = 0x200a
const codeNarrowNoBreakSpace = 0x202f
const codeMediumMathematicalSpace = 0x205f
const codeIdeographicSpace = 0x3000
const codeDoubleQuoteLeft = 0x201c // “
const codeDoubleQuoteRight = 0x201d // ”
const codeQuoteLeft = 0x2018 // ‘
const codeQuoteRight = 0x2019 // ’
const codeGraveAccent = 0x0060 // `
const codeAcuteAccent = 0x00b4 // ´



export function isHex(code: number): boolean {
    return (
        (code >= codeZero && code <= codeNine) ||
        (code >= codeUppercaseA && code <= codeUppercaseF) ||
        (code >= codeLowercaseA && code <= codeLowercaseF)
    )
}

export function isDigit(code: number): boolean {
    return code >= codeZero && code <= codeNine
}

export function isValidStringCharacter(code: number): boolean {
    return code >= 0x20 && code <= 0x10ffff
}

export function isDelimiter(char: string): boolean {
    return regexDelimiter.test(char)
}

const regexDelimiter = /^[,:[\]/{}()\n+]$/
const regexUnquotedStringDelimiter = /^[,[\]/{}\n+]$/
export const regexFunctionNameCharStart = /^[a-zA-Z_$]$/
export const regexFunctionNameChar = /^[a-zA-Z_$0-9]$/

// matches "https://" and other schemas
export const regexUrlStart = /^(http|https|ftp|mailto|file|data|irc):\/\/$/

export const regexUrlChar = /^[A-Za-z0-9-._~:/?#@!$&'()*+;=]$/


/// ------------------------------------------------
export function isUnquotedStringDelimiter(char: string): boolean {
    return regexUnquotedStringDelimiter.test(char)
}

export function isStartOfValue(char: string): boolean {
    return regexStartOfValue.test(char) || (char && isQuote(char.charCodeAt(0)))
}

const regexStartOfValue = /^[[{\w-]$/

export function isControlCharacter(code: number) {
    return (
        code === codeNewline ||
        code === codeReturn ||
        code === codeTab ||
        code === codeBackspace ||
        code === codeFormFeed
    )
}

/**
 * Check if the given character is a whitespace character like space, tab, or
 * newline
 */
export function isWhitespace(code: number): boolean {
    return code === codeSpace || code === codeNewline || code === codeTab || code === codeReturn
}

/**
 * Check if the given character is a whitespace character like space or tab,
 * but NOT a newline
 */
export function isWhitespaceExceptNewline(code: number): boolean {
    return code === codeSpace || code === codeTab || code === codeReturn
}

/**
 * Check if the given character is a special whitespace character, some
 * unicode variant
 */
export function isSpecialWhitespace(code: number): boolean {
    return (
        code === codeNonBreakingSpace ||
        (code >= codeEnQuad && code <= codeHairSpace) ||
        code === codeNarrowNoBreakSpace ||
        code === codeMediumMathematicalSpace ||
        code === codeIdeographicSpace
    )
}

/**
 * Test whether the given character is a quote or double quote character.
 * Also tests for special variants of quotes.
 */
export function isQuote(code: number): boolean {
    // the first check double quotes, since that occurs most often
    return isDoubleQuoteLike(code) || isSingleQuoteLike(code)
}

/**
 * Test whether the given character is a double quote character.
 * Also tests for special variants of double quotes.
 */
export function isDoubleQuoteLike(code: number): boolean {
    // the first check double quotes, since that occurs most often
    return code === codeDoubleQuote || code === codeDoubleQuoteLeft || code === codeDoubleQuoteRight
}

/**
 * Test whether the given character is a double quote character.
 * Does NOT test for special variants of double quotes.
 */
export function isDoubleQuote(code: number): boolean {
    return code === codeDoubleQuote
}

/**
 * Test whether the given character is a single quote character.
 * Also tests for special variants of single quotes.
 */
export function isSingleQuoteLike(code: number): boolean {
    return (
        code === codeQuote ||
        code === codeQuoteLeft ||
        code === codeQuoteRight ||
        code === codeGraveAccent ||
        code === codeAcuteAccent
    )
}

/**
 * Test whether the given character is a single quote character.
 * Does NOT test for special variants of single quotes.
 */
export function isSingleQuote(code: number): boolean {
    return code === codeQuote
}

/**
 * Strip last occurrence of textToStrip from text
 */
export function stripLastOccurrence(
    text: string,
    textToStrip: string,
    stripRemainingText = false
): string {
    const index = text.lastIndexOf(textToStrip)
    return index !== -1
        ? text.substring(0, index) + (stripRemainingText ? '' : text.substring(index + 1))
        : text
}
const controlCharacters: { [key: string]: string } = {
    '\b': '\\b',
    '\f': '\\f',
    '\n': '\\n',
    '\r': '\\r',
    '\t': '\\t'
}

// map with all escape characters
const escapeCharacters: { [key: string]: string } = {
    '"': '"',
    '\\': '\\',
    '/': '/',
    b: '\b',
    f: '\f',
    n: '\n',
    r: '\r',
    t: '\t'
    // note that \u is handled separately in parseString()
}
function atEndOfBlockComment(text: string, i: number) {
    return text[i] === '*' && text[i + 1] === '/'
}
export class JSONRepairError extends Error {
    position: number

    constructor(message: string, position: number) {
        super(`${message} at position ${position}`)

        this.position = position
    }
}

export function insertBeforeLastWhitespace(text: string, textToInsert: string): string {
    let index = text.length

    if (!isWhitespace(text.charCodeAt(index - 1))) {
        // no trailing whitespaces
        return text + textToInsert
    }

    while (isWhitespace(text.charCodeAt(index - 1))) {
        index--
    }

    return text.substring(0, index) + textToInsert + text.substring(index)
}

export function removeAtIndex(text: string, start: number, count: number) {
    return text.substring(0, start) + text.substring(start + count)
}

/**
 * Test whether a string ends with a newline or comma character and optional whitespace
 */
export function endsWithCommaOrNewline(text: string): boolean {
    return /[,\n][ \t\r]*$/.test(text)
}

function prevNonWhitespaceIndex(start: number , text ): number {
    let prev = start

    while (prev > 0 && isWhitespace(text.charCodeAt(prev))) {
        prev--
    }

    return prev
}

function atEndOfNumber( text , i ) {
    return i >= text.length || isDelimiter(text[i]) || isWhitespace(text.charCodeAt(i))
}

function repairNumberEndingWithNumericSymbol(start: number , output , text , i  ) {
    // repair numbers cut off at the end
    // this will only be called when we end after a '.', '-', or 'e' and does not
    // change the number more than it needs to make it valid JSON
    output += `${text.slice(start, i)}0`
}

function throwInvalidCharacter(char: string , i ) {
    throw new JSONRepairError(`Invalid character ${JSON.stringify(char)}`, i)
}

function throwUnexpectedCharacter(text ,  i ) {
    throw new JSONRepairError(`Unexpected character ${JSON.stringify(text[i])}`, i)
}

function throwUnexpectedEnd( text ) {
    throw new JSONRepairError('Unexpected end of json string', text.length)
}

function throwObjectKeyExpected(  i ) {
    throw new JSONRepairError('Object key expected', i)
}

function throwColonExpected(i ) {
    throw new JSONRepairError('Colon expected', i)
}

function throwInvalidUnicodeCharacter(text, i ) {
    const chars = text.slice(i, i + 6)
    throw new JSONRepairError(`Invalid unicode character "${chars}"`, i)
}
/// ------------------------------------------------
/**
 * Parse a JSON string and return the parsed value
 */

export function jsonrepair(text: string): string {
    let i = 0 // current index in text
    let output = '' // generated output

    const processed = parseValue()
    if (!processed) {
        throwUnexpectedEnd(text )
    }

    const processedComma = parseCharacter(codeComma)
    if (processedComma) {
        parseWhitespaceAndSkipComments()
    }

    if (isStartOfValue(text[i]) && endsWithCommaOrNewline(output)) {
        // start of a new value after end of the root level object: looks like
        // newline delimited JSON -> turn into a root level array
        if (!processedComma) {
            // repair missing comma
            output = insertBeforeLastWhitespace(output, ',')
        }

        parseNewlineDelimitedJSON()
    } else if (processedComma) {
        // repair: remove trailing comma
        output = stripLastOccurrence(output, ',')
    }

    // repair redundant end quotes
    while (text.charCodeAt(i) === codeClosingBrace || text.charCodeAt(i) === codeClosingBracket) {
        i++
        parseWhitespaceAndSkipComments()
    }

    if (i >= text.length) {
        return output
    }

    throwUnexpectedCharacter( text , i )
    function parseValue(): boolean {
        parseWhitespaceAndSkipComments()
        const processed =
            parseObject() ||
            parseArray() ||
            parseString() ||
            parseNumber() ||
            parseKeywords() ||
            parseUnquotedString(false) ||
            parseRegex()
        parseWhitespaceAndSkipComments()

        return processed
    }

    function    parseWhitespaceAndSkipComments(skipNewline = true): boolean {
        const start = i

        let changed = parseWhitespace(skipNewline)
        do {
            changed = parseComment()
            if (changed) {
                changed = parseWhitespace(skipNewline)
            }
        } while (changed)

        return i > start
    }

    function parseWhitespace(skipNewline: boolean): boolean {
        const _isWhiteSpace = skipNewline ? isWhitespace : isWhitespaceExceptNewline
        let whitespace = ''

        while (true) {
            const c = text.charCodeAt(i)
            if (_isWhiteSpace(c)) {
                whitespace += text[i]
                i++
            } else if (isSpecialWhitespace(c)) {
                // repair special whitespace
                whitespace += ' '
                i++
            } else {
                break
            }
        }

        if (whitespace.length > 0) {
            output += whitespace
            return true
        }

        return false
    }

    function parseComment(): boolean {
        // find a block comment '/* ... */'
        if (text.charCodeAt(i) === codeSlash && text.charCodeAt(i + 1) === codeAsterisk) {
            // repair block comment by skipping it
            while (i < text.length && !atEndOfBlockComment(text, i)) {
                i++
            }
            i += 2

            return true
        }

        // find a line comment '// ...'
        if (text.charCodeAt(i) === codeSlash && text.charCodeAt(i + 1) === codeSlash) {
            // repair line comment by skipping it
            while (i < text.length && text.charCodeAt(i) !== codeNewline) {
                i++
            }

            return true
        }

        return false
    }

    function parseCharacter(code: number): boolean {
        if (text.charCodeAt(i) === code) {
            output += text[i]
            i++
            return true
        }

        return false
    }

    function skipCharacter(code: number): boolean {
        if (text.charCodeAt(i) === code) {
            i++
            return true
        }

        return false
    }

    function skipEscapeCharacter(): boolean {
        return skipCharacter(codeBackslash)
    }
    function skipEllipsis(): boolean {
        parseWhitespaceAndSkipComments()

        if (
            text.charCodeAt(i) === codeDot &&
            text.charCodeAt(i + 1) === codeDot &&
            text.charCodeAt(i + 2) === codeDot
        ) {
            // repair: remove the ellipsis (three dots) and optionally a comma
            i += 3
            parseWhitespaceAndSkipComments()
            skipCharacter(codeComma)

            return true
        }

        return false
    }

    function parseObject(): boolean {
        if (text.charCodeAt(i) === codeOpeningBrace) {
            output += '{'
            i++
            parseWhitespaceAndSkipComments()

            // repair: skip leading comma like in {, message: "hi"}
            if (skipCharacter(codeComma)) {
                parseWhitespaceAndSkipComments()
            }

            let initial = true
            while (i < text.length && text.charCodeAt(i) !== codeClosingBrace) {
                let processedComma: boolean
                if (!initial) {
                    processedComma = parseCharacter(codeComma)
                    if (!processedComma) {
                        // repair missing comma
                        output = insertBeforeLastWhitespace(output, ',')
                    }
                    parseWhitespaceAndSkipComments()
                } else {
                    processedComma = true
                    initial = false
                }

                skipEllipsis()

                const processedKey = parseString() || parseUnquotedString(true)
                if (!processedKey) {
                    if (
                        text.charCodeAt(i) === codeClosingBrace ||
                        text.charCodeAt(i) === codeOpeningBrace ||
                        text.charCodeAt(i) === codeClosingBracket ||
                        text.charCodeAt(i) === codeOpeningBracket ||
                        text[i] === undefined
                    ) {
                        // repair trailing comma
                        output = stripLastOccurrence(output, ',')
                    } else {
                        throwObjectKeyExpected(i )
                    }
                    break
                }

                parseWhitespaceAndSkipComments()
                const processedColon = parseCharacter(codeColon)
                const truncatedText = i >= text.length
                if (!processedColon) {
                    if (isStartOfValue(text[i]) || truncatedText) {
                        // repair missing colon
                        output = insertBeforeLastWhitespace(output, ':')
                    } else {
                        throwColonExpected(i )
                    }
                }
                const processedValue = parseValue()
                if (!processedValue) {
                    if (processedColon || truncatedText) {
                        // repair missing object value
                        output += 'null'
                    } else {
                        throwColonExpected(i )
                    }
                }
            }

            if (text.charCodeAt(i) === codeClosingBrace) {
                output += '}'
                i++
            } else {
                // repair missing end bracket
                output = insertBeforeLastWhitespace(output, '}')
            }

            return true
        }

        return false
    }

    function parseArray(): boolean {
        if (text.charCodeAt(i) === codeOpeningBracket) {
            output += '['
            i++
            parseWhitespaceAndSkipComments()

            // repair: skip leading comma like in [,1,2,3]
            if (skipCharacter(codeComma)) {
                parseWhitespaceAndSkipComments()
            }

            let initial = true
            while (i < text.length && text.charCodeAt(i) !== codeClosingBracket) {
                if (!initial) {
                    const processedComma = parseCharacter(codeComma)
                    if (!processedComma) {
                        // repair missing comma
                        output = insertBeforeLastWhitespace(output, ',')
                    }
                } else {
                    initial = false
                }

                skipEllipsis()

                const processedValue = parseValue()
                if (!processedValue) {
                    // repair trailing comma
                    output = stripLastOccurrence(output, ',')
                    break
                }
            }

            if (text.charCodeAt(i) === codeClosingBracket) {
                output += ']'
                i++
            } else {
                // repair missing closing array bracket
                output = insertBeforeLastWhitespace(output, ']')
            }

            return true
        }

        return false
    }

    function parseNewlineDelimitedJSON() {
        // repair NDJSON
        let initial = true
        let processedValue = true
        while (processedValue) {
            if (!initial) {
                // parse optional comma, insert when missing
                const processedComma = parseCharacter(codeComma)
                if (!processedComma) {
                    // repair: add missing comma
                    output = insertBeforeLastWhitespace(output, ',')
                }
            } else {
                initial = false
            }

            processedValue = parseValue()
        }

        if (!processedValue) {
            // repair: remove trailing comma
            output = stripLastOccurrence(output, ',')
        }

        // repair: wrap the output inside array brackets
        output = `[\n${output}\n]`
    }

    function parseString(stopAtDelimiter = false, stopAtIndex = -1): boolean {
        let skipEscapeChars = text.charCodeAt(i) === codeBackslash
        if (skipEscapeChars) {
            // repair: remove the first escape character
            i++
            skipEscapeChars = true
        }

        if (isQuote(text.charCodeAt(i))) {
            // double quotes are correct JSON,
            // single quotes come from JavaScript for example, we assume it will have a correct single end quote too
            // otherwise, we will match any double-quote-like start with a double-quote-like end,
            // or any single-quote-like start with a single-quote-like end
            const isEndQuote = isDoubleQuote(text.charCodeAt(i))
                ? isDoubleQuote
                : isSingleQuote(text.charCodeAt(i))
                    ? isSingleQuote
                    : isSingleQuoteLike(text.charCodeAt(i))
                        ? isSingleQuoteLike
                        : isDoubleQuoteLike

            const iBefore = i
            const oBefore = output.length

            let str = '"'
            i++

            while (true) {
                if (i >= text.length) {
                    // end of text, we are missing an end quote

                    const iPrev = prevNonWhitespaceIndex(i - 1 , text )
                    if (!stopAtDelimiter && isDelimiter(text.charAt(iPrev))) {
                        // if the text ends with a delimiter, like ["hello],
                        // so the missing end quote should be inserted before this delimiter
                        // retry parsing the string, stopping at the first next delimiter
                        i = iBefore
                        output = output.substring(0, oBefore)

                        return parseString(true)
                    }

                    // repair missing quote
                    str = insertBeforeLastWhitespace(str, '"')
                    output += str

                    return true
                    // biome-ignore lint/style/noUselessElse: <explanation>
                } else if (i === stopAtIndex) {
                    // use the stop index detected in the first iteration, and repair end quote
                    str = insertBeforeLastWhitespace(str, '"')
                    output += str

                    return true
                    // biome-ignore lint/style/noUselessElse: <explanation>
                } else if (isEndQuote(text.charCodeAt(i))) {
                    // end quote
                    // let us check what is before and after the quote to verify whether this is a legit end quote
                    const iQuote = i
                    const oQuote = str.length
                    str += '"'
                    i++
                    output += str

                    parseWhitespaceAndSkipComments(false)

                    if (
                        stopAtDelimiter ||
                        i >= text.length ||
                        isDelimiter(text.charAt(i)) ||
                        isQuote(text.charCodeAt(i)) ||
                        isDigit(text.charCodeAt(i))
                    ) {
                        // The quote is followed by the end of the text, a delimiter,
                        // or a next value. So the quote is indeed the end of the string.
                        parseConcatenatedString()

                        return true
                    }

                    const iPrevChar = prevNonWhitespaceIndex(iQuote - 1 , text  )
                    const prevChar = text.charAt(iPrevChar)

                    if (prevChar === ',') {
                        // A comma followed by a quote, like '{"a":"b,c,"d":"e"}'.
                        // We assume that the quote is a start quote, and that the end quote
                        // should have been located right before the comma but is missing.
                        i = iBefore
                        output = output.substring(0, oBefore)

                        return parseString(false, iPrevChar)
                    }

                    if (isDelimiter(prevChar)) {
                        // This is not the right end quote: it is preceded by a delimiter,
                        // and NOT followed by a delimiter. So, there is an end quote missing
                        // parse the string again and then stop at the first next delimiter
                        i = iBefore
                        output = output.substring(0, oBefore)

                        return parseString(true)
                    }

                    // revert to right after the quote but before any whitespace, and continue parsing the string
                    output = output.substring(0, oBefore)
                    i = iQuote + 1

                    // repair unescaped quote
                    str = `${str.substring(0, oQuote)}\\${str.substring(oQuote)}`
                } else if (stopAtDelimiter && isUnquotedStringDelimiter(text[i])) {
                    // we're in the mode to stop the string at the first delimiter
                    // because there is an end quote missing

                    // test start of an url like "https://..." (this would be parsed as a comment)
                    if (
                        text.charCodeAt(i - 1) === codeColon &&
                        regexUrlStart.test(text.substring(iBefore + 1, i + 2))
                    ) {
                        while (i < text.length && regexUrlChar.test(text[i])) {
                            str += text[i]
                            i++
                        }
                    }

                    // repair missing quote
                    str = insertBeforeLastWhitespace(str, '"')
                    output += str

                    parseConcatenatedString()

                    return true
                } else if (text.charCodeAt(i) === codeBackslash) {
                    // handle escaped content like \n or \u2605
                    const char = text.charAt(i + 1)
                    const escapeChar = escapeCharacters[char]
                    if (escapeChar !== undefined) {
                        str += text.slice(i, i + 2)
                        i += 2
                    } else if (char === 'u') {
                        let j = 2
                        while (j < 6 && isHex(text.charCodeAt(i + j))) {
                            j++
                        }

                        if (j === 6) {
                            str += text.slice(i, i + 6)
                            i += 6
                        } else if (i + j >= text.length) {
                            // repair invalid or truncated unicode char at the end of the text
                            // by removing the unicode char and ending the string here
                            i = text.length
                        } else {
                            throwInvalidUnicodeCharacter(text , i )
                        }
                    } else {
                        // repair invalid escape character: remove it
                        str += char
                        i += 2
                    }
                } else {
                    // handle regular characters
                    const char = text.charAt(i)
                    const code = text.charCodeAt(i)

                    if (code === codeDoubleQuote && text.charCodeAt(i - 1) !== codeBackslash) {
                        // repair unescaped double quote
                        str += `\\${char}`
                        i++
                    } else if (isControlCharacter(code)) {
                        // unescaped control character
                        str += controlCharacters[char]
                        i++
                    } else {
                        if (!isValidStringCharacter(code)) {
                            throwInvalidCharacter(char , i)
                        }
                        str += char
                        i++
                    }
                }

                if (skipEscapeChars) {
                    // repair: skipped escape character (nothing to do)
                    skipEscapeCharacter()
                }
            }
        }

        return false
    }

    function parseConcatenatedString(): boolean {
        let processed = false

        parseWhitespaceAndSkipComments()
        while (text.charCodeAt(i) === codePlus) {
            processed = true
            i++
            parseWhitespaceAndSkipComments()

            // repair: remove the end quote of the first string
            output = stripLastOccurrence(output, '"', true)
            const start = output.length
            const parsedStr = parseString()
            if (parsedStr) {
                // repair: remove the start quote of the second string
                output = removeAtIndex(output, start, 1)
            } else {
                // repair: remove the + because it is not followed by a string
                output = insertBeforeLastWhitespace(output, '"')
            }
        }

        return processed
    }

    function parseNumber(): boolean {
        const start = i
        if (text.charCodeAt(i) === codeMinus) {
            i++
            if (atEndOfNumber( text, i )) {
                repairNumberEndingWithNumericSymbol(start ,output , text , i )
                return true
            }
            if (!isDigit(text.charCodeAt(i))) {
                i = start
                return false
            }
        }

        // Note that in JSON leading zeros like "00789" are not allowed.
        // We will allow all leading zeros here though and at the end of parseNumber
        // check against trailing zeros and repair that if needed.
        // Leading zeros can have meaning, so we should not clear them.
        while (isDigit(text.charCodeAt(i))) {
            i++
        }

        if (text.charCodeAt(i) === codeDot) {
            i++
            if (atEndOfNumber(text , i )) {
                repairNumberEndingWithNumericSymbol(start , output , text , i  )
                return true
            }
            if (!isDigit(text.charCodeAt(i))) {
                i = start
                return false
            }
            while (isDigit(text.charCodeAt(i))) {
                i++
            }
        }

        if (text.charCodeAt(i) === codeLowercaseE || text.charCodeAt(i) === codeUppercaseE) {
            i++
            if (text.charCodeAt(i) === codeMinus || text.charCodeAt(i) === codePlus) {
                i++
            }
            if (atEndOfNumber( text, i )) {
                repairNumberEndingWithNumericSymbol(start , output , text , i  )
                return true
            }
            if (!isDigit(text.charCodeAt(i))) {
                i = start
                return false
            }
            while (isDigit(text.charCodeAt(i))) {
                i++
            }
        }

        // if we're not at the end of the number by this point, allow this to be parsed as another type
        if (!atEndOfNumber( text, i )) {
            i = start
            return false
        }

        if (i > start) {
            // repair a number with leading zeros like "00789"
            const num = text.slice(start, i)
            const hasInvalidLeadingZero = /^0\d/.test(num)

            output += hasInvalidLeadingZero ? `"${num}"` : num
            return true
        }

        return false
    }

    function parseKeywords(): boolean {
        return (
            parseKeyword('true', 'true') ||
            parseKeyword('false', 'false') ||
            parseKeyword('null', 'null') ||
            // repair Python keywords True, False, None
            parseKeyword('True', 'true') ||
            parseKeyword('False', 'false') ||
            parseKeyword('None', 'null')
        )
    }

    function parseKeyword(name: string, value: string): boolean {
        if (text.slice(i, i + name.length) === name) {
            output += value
            i += name.length
            return true
        }

        return false
    }

    function parseUnquotedString(isKey: boolean) {
        // note that the symbol can end with whitespaces: we stop at the next delimiter
        // also, note that we allow strings to contain a slash / in order to support repairing regular expressions
        const start = i

        if (regexFunctionNameCharStart.test(text[i])) {
            while (i < text.length && regexFunctionNameChar.test(text[i])) {
                i++
            }

            let j = i
            while (isWhitespace(text.charCodeAt(j))) {
                j++
            }

            if (text[j] === '(') {
                // repair a MongoDB function call like NumberLong("2")
                // repair a JSONP function call like callback({...});
                i = j + 1

                parseValue()

                if (text.charCodeAt(i) === codeCloseParenthesis) {
                    // repair: skip close bracket of function call
                    i++
                    if (text.charCodeAt(i) === codeSemicolon) {
                        // repair: skip semicolon after JSONP call
                        i++
                    }
                }

                return true
            }
        }

        while (
            i < text.length &&
            !isUnquotedStringDelimiter(text[i]) &&
            !isQuote(text.charCodeAt(i)) &&
            (!isKey || text.charCodeAt(i) !== codeColon)
            ) {
            i++
        }

        // test start of an url like "https://..." (this would be parsed as a comment)
        if (text.charCodeAt(i - 1) === codeColon && regexUrlStart.test(text.substring(start, i + 2))) {
            while (i < text.length && regexUrlChar.test(text[i])) {
                i++
            }
        }

        if (i > start) {
            // repair unquoted string
            // also, repair undefined into null

            // first, go back to prevent getting trailing whitespaces in the string
            while (isWhitespace(text.charCodeAt(i - 1)) && i > 0) {
                i--
            }

            const symbol = text.slice(start, i)
            output += symbol === 'undefined' ? 'null' : JSON.stringify(symbol)

            if (text.charCodeAt(i) === codeDoubleQuote) {
                // we had a missing start quote, but now we encountered the end quote, so we can skip that one
                i++
            }

            return true
        }
    }

    function parseRegex() {
        if (text[i] === '/') {
            const start = i
            i++

            while (i < text.length && (text[i] !== '/' || text[i - 1] === '\\')) {
                i++
            }
            i++

            output += `"${text.substring(start, i)}"`

            return true
        }
    }

}

